// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Jeff Stewart
//
// Implements a probe that connects to a POP-over-SSL server and tests the
// ability to login using POP USER/PASS and retrieve mail.  By default
// retrieves only one message, but can be optionally configured to fetch
// all messages.

#ifndef POPDIAG_PROBES_POPPROBE_H__
#define POPDIAG_PROBES_POPPROBE_H__

#include <QtNetwork/QHostInfo>
#include <QtNetwork/QTcpSocket>
#include <QtCore/QIODevice>
#include <QtCore/QSet>
#include <QtCore/QTextStream>
#include <QtCore/QTimer>
#include <QtCore/QTime>
#include <QtNetwork/QtNetwork>

#include "probes/linereader.h"
#include "probes/multiprobe.h"
#include "probes/probe.h"

namespace popdiag {

class PopResponseReader;

typedef enum { POP_RESPONSE_OK,
               POP_RESPONSE_ERR,
               POP_RESPONSE_MALFORMED,
               POP_SOCKET_DROPPED,
               POP_RESPONSE_TIMEOUT } PopResponseType;

// Spawns pop probes for each IP address detected by MultiProbe.
class PopProbeFactory : public MultiProbeFactory {
 public:
  // Creates a factory that will merge these arguments with those of
  // CreateProbe and create PopProbes on demand.
  PopProbeFactory(const QString &username,
                  const QString &password,
                  const QString &pop_server_name,
                  bool pull_all_mail);

  // Merges these arguments with those of the constructor and returns a
  // constructed PopProbe.
  virtual Probe *CreateProbe(const QHostAddress &addr, bool do_complete_probe);
 private:
  const QString username_;
  const QString password_;
  const QString pop_server_name_;
  const bool pull_all_mail_;
};

// Probes one IP address on port 995 talking POPS.  Probes only as far
// as banner verification unless do_complete_probe is true.  If pull_all_mail
// is true, we'll retrieve all mail.  Otherwise we'll pull the first message.
class PopProbe : public Probe {
  Q_OBJECT
 public:
  // Creates a probe to probe pop over SSL on port 995.
  // username: The username to present using the POP USER command.
  // password: The password to present using the POP PASS command.
  // pop_server_name: The DNS name of the POP server.  Used to verify the SSL
  //                  certificate.
  // pull_all_mail: If true, RETRieve all mail.  Otherwise just the first msg.
  // do_complete_probe: If false, stop after banner validation.  Otherwise
  //                    do a full probe.
  PopProbe(const QString &username,
           const QString &password,
           const QString &pop_server_name,
           const QHostAddress &host_address,
           bool pull_all_mail,
           bool do_complete_probe);
  virtual ~PopProbe();

  // Fetch progress made in this probe.
  void GetProgress(int *progress, int *completed);

  // Called by the base class to start the probe
  void StartProbe();

  // Get a display string describing the results of this probe.
  QString GetDisplayText();

  // Retrieve a numeric representation of this fault.
  // 0 is reserved for "success".  Most other values are probe-specific.
  int GetFaultId();

  // Get a string representation of this probe's class.
  // Can be used with GetFaultId() to construct support urls, though
  // it is not currently used for this.
  inline QString GetProbeId() { return "pop"; }

  // Values in the common range must match multiprobe's values.
  typedef enum {
    // begin common range
    OK = 0,
    UNKNOWN = 1,
    RESOLVE_FAILED = 2,
    // end common range

    CONNECTION_REFUSED,
    CONNECT_TIMED_OUT,
    SERVER_CLOSED_PREMATURELY,
    SOCKET_ERROR,
    POP_SERVER_CAFILE_NOT_FOUND,
    POP_SERVER_CERT_SELFSIGNED,
    POP_SERVER_CERT_EXPIRED,
    POP_SERVER_CERT_NOTYETVALID,
    POP_SERVER_CERT_CANNOT_VALIDATE,
    POP_SERVER_CERT_NAME_MISMATCH,
    POP_SERVER_SHEDDING_LOAD,
    POP_SERVER_RESPONSE_MALFORMED,
    POP_RESPONSE_TIMED_OUT,
    POP_CAPA_USER_MISSING,
    POP_USER_REJECTED,
    POP_USER_BACKEND_OFFLINE,
    POP_LOGIN_FAILED_TEMP_SYSTEM_FAILURE,
    POP_NOT_ENABLED_FOR_USER,
    POP_LOGIN_FAILED_PERM_SYSTEM_FAILURE,
    POP_LOGIN_FAILED_ABUSE,
    POP_LOGIN_FAILED_GEO_LOCKOUT,
    POP_LOGIN_FAILED_BAD_CREDS,
    POP_RETR_FAILED,
    POP_STAT_FAILED,
    POP_UIDL_FAILED,
    POP_LIST_FAILED,
  } PopProbeFault;

  private slots:
  void OnSslError(const QList<QSslError> &errors);
  void OnSslTimeout();
  void OnSslEstablished();
  void OnError(QAbstractSocket::SocketError err);

  void OnConnected();
  void OnDisconnected();
  void OnConnectTimedOut();
  void OnConnectError(QAbstractSocket::SocketError err);
  void OnCommandResponseReceived(PopResponseReader *reader);

 private:
  typedef enum {
    INIT,
    CONNECT,
    NEGOTIATE_SSL,
    BANNER,
    CAPA,
    USER,
    PASS,
    STAT,
    UIDL,
    LIST,
    RETR,
    // since we issue RETR, we NEVER issue QUIT.  We just drop.
  } ProbeState;

  const char *StepNameFromState();
  PopResponseReader *GetReader(bool is_long_response, bool truncate_response);

  void BeginSecureConversation();

  void ProcessOkResponse(PopResponseReader *reader);
  void ProcessErrResponse(PopResponseReader *reader);

  // Do enhanced failure logging for a retrieve command.
  void LogRetrFailure(PopResponseReader *reader);

  void SetState(ProbeState t);
  void Cleanup();

  void ProbeDone(PopProbeFault f);

  // These are called in sequence, with the invocation
  // of the next call placed from the corresponding
  // FinishProbeX above.  The exception is Connect, which
  // uses a variety of callbacks to terminate the various
  // error cases.
  void StartProbeConnect();
  void StartProbeNegotiateSsl();

  void StartProbeBanner();
  void BannerOk(PopResponseReader *reader);
  void BannerRejected(PopResponseReader *reader);
  void NatCheck(PopResponseReader *banner_reader);

  void StartProbeCapa();
  void CapaOk(PopResponseReader *reader);

  void StartProbeUser();
  void UserOk(PopResponseReader *reader);

  void StartProbePass();
  void PassOk(PopResponseReader *reader);
  void PassRejected(PopResponseReader *reader);

  void StartProbeStat();
  void StatOk(PopResponseReader *reader);

  void StartProbeUidl();
  void UidlOk(PopResponseReader *reader);

  void StartProbeList();
  void ListOk(PopResponseReader *reader);

  void StartProbeRetr();
  void RetrOk(PopResponseReader *reader);

  void ScheduleNextRetr();

  static const int kTimeoutMs;
  const QString username_;
  const QString password_;
  const QString pop_server_;

  // The base object for the socket to use.
  scoped_ptr<RenamableSslSocket> base_socket_;

  // Read and write from here.
  RenamableSslSocket *socket_;

  // How far in our state machine have we gotten?
  ProbeState state_;

  // The highest value state_ will ever get.
  const ProbeState max_state_;

  // An enumerated value indicating the fault we've found.
  PopProbeFault fault_;

  // If true, RETRieve all mail during the POP session.
  // otherwise fetch the first message.
  bool pull_all_mail_;

  // If true, stop the probe when the banner is read.
  bool probe_banner_only_;

  // If true, we requested socket teardown.  Ignore socket errors.
  bool we_closed_;

  // How many bytes have we fetched?
  qulonglong bytes_retrieved_;

  // How many bytes do we expect we will fetch?
  qulonglong bytes_expected_;

  int msgs_in_mailbox_;
  int msgs_retrieved_;

  // The UIDLs of the messages in the mailbox,
  // indexed by the 0-based message number (i.e. not pop starts-at-1 index).
  QList<QString> uidls_;

  // The lengths of the messages in the mailbox,
  // indexed by the 0-based message number (i.e. not pop starts-at-1 index).
  QList<int> lengths_;

  // The TCP address we're going to connect to in this probe.
  const QHostAddress target_address_;

  // Times out our connect attempts
  QTimer *connect_timer_;

  // Times out SSL negotiation.
  QTimer *ssl_negotiate_timer_;

  // Times our retrieves.
  QTime *fetch_timer_;

  // The current response reader, or NULL if we're not reading right now.
  PopResponseReader *reader_;
};

// A PopResponseReader takes over reading a response from the POP server.
// It will generate its Finished() signal when done, on error, or on timeout.
class PopResponseReader : public QObject {
  Q_OBJECT
 public:
  // Create an object that can issue a command and read a POP response.
  // parent: takes memory ownership of this object.
  // src: the socket to read from
  // timeout_in_millis: If no response (line-wise) in this amount of time,
  //                    then declare the response timed out.
  // long_response: If true, after a successful status line we expect a
  //                series of extended response lines terminated by
  //                a line with dot by itself.
  // truncate_response: Whether we need the complete response (for UIDL etc.)
  //                    or not (for RETR etc.).

  PopResponseReader(QObject *parent,
                    RenamableSslSocket *src,
                    int timeout_in_millis,
                    bool long_response,
                    bool truncate_response);
  ~PopResponseReader();

  // Send a command and start reading the response.
  // The empty string is a special case.  It will start reading a response
  // without sending a command.
  void SendCommand(const QString &cmd);

  // What kind of response did we get?
  PopResponseType response_type() const { return response_type_; }

  // Was an extended response code present? (AUTH, SYS/PERM, etc)
  const QString extended_code() const { return extended_code_; }

  // Returns the part of the first line of the response after the +OK or -ERR.
  const QString response_line_comment() const { return response_comment_; }

  // Describe the response for the diagnostic log.  Excerpt
  // any long response.
  QString describe();

  // Return the list of lines in the "long response" part, after the status
  // line and before the dot by itself.
  const QList<QString> &long_response_lines() const { return response_lines_; }

 public slots:
  // Try to pull off any input.
  void TryRead();

 signals:
  // This reader is done.  Use the accessors to dispatch the result.
  void ResponseRead(PopResponseReader *me);

 private slots:
  void DataAvailable(LineReadResult r);

 private:
  // Clean up and emit this result code.
  void Done(PopResponseType result);

  void ProcessLine(const QString &line);
  void ProcessFirstLine(const QString &line);
  void ProcessFollowupLine(const QString &line);

  RenamableSslSocket *socket_;
  QList<QString> response_lines_;
  PopResponseType response_type_;
  QString extended_code_;
  QString response_comment_;

  bool anything_read_;
  LineReader reader_;
  const bool long_response_;
  bool truncate_response_;
};

}  // namespace popdiag

#endif  // POPDIAG_PROBES_POPPROBE_H__
